﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using grounding.nao;
using grounding.nao.sensors;
using grounding.parser;
using Accord.MachineLearning.VectorMachines;
using Accord.Statistics.Kernels;
using Accord.MachineLearning.VectorMachines.Learning;
using grounding.parser.Parsers;
using Accord.Statistics.Analysis;

namespace grounding.grounder
{
    public class SVMGrounder : IGrounder
    {
        protected Dictionary<Category, KernelSupportVectorMachine> SVMs = new Dictionary<Category, KernelSupportVectorMachine>();
        protected Dictionary<Category, KernelPrincipalComponentAnalysis> PCAs = new Dictionary<Category, KernelPrincipalComponentAnalysis>();
        protected Dictionary<Category, TrainingData> previousData = new Dictionary<Category, TrainingData>();
        protected IKernel svmKernel;
        protected IKernel pcaKernel;

        #region Properties

        /// <summary>
        /// Sigma value used to create the Gaussian Kernel.  This value adjusts how well the function is fit
        /// to the training data.  This value can cause overfitting or underfitting.
        /// </summary>
        public double SVMSigma { get { return svmSigma; } }
        private double svmSigma;

        public double PCASigma { get { return pcaSigma; } }
        private double pcaSigma;

        /// <summary>
        /// Complexity used in the Sequential Minimal Optimization algorithm.  This value adjusts how well 
        /// the function is fit to the training data.  This value can cause overfitting or underfitting.
        /// </summary>
        public double Complexity { get { return complexity; } }
        private double complexity;

        /// <summary>
        /// Number of features to use while learning.
        /// </summary>
        public int FeatureCount { get { return featureCount; } }
        private int featureCount;

        #endregion

        #region Constructors

        /// <summary>
        /// Creates a grounder based on Support Vector Machines using Gaussian kernels.
        /// </summary>
        /// <param name="sigma">the sigma value used to create the Gaussian kernel</param>
        /// <param name="complexity">the complexity value used in the Sequential Minimal Optimization algorithm</param>
        /// <param name="numFeatures">the number of features to use from PCA.  Specify -1 to not use PCA and use all features.</param>
        public SVMGrounder(double svmSigma = 1.3, double pcaSigma = .3, double complexity = 1.0, int numFeatures = -1)
        {
            this.svmSigma = svmSigma;
            svmKernel = new Gaussian(svmSigma);
            this.pcaSigma = pcaSigma;
            pcaKernel = new Gaussian(pcaSigma);
            this.complexity = complexity;
            this.featureCount = numFeatures;
        }
        public SVMGrounder(IKernel svmKernel, IKernel pcaKernel, int numFeatures = 0)
        {
            this.svmKernel = svmKernel;
            this.pcaKernel = pcaKernel;
            this.featureCount = numFeatures;
            svmSigma = pcaSigma = complexity = 0;
        }

        #endregion

        #region Utility Methods

        /// <summary>
        /// Converts from a double[,] to a double[][]
        /// </summary>
        /// <param name="input">The multi-dimensional array to convert</param>
        /// <returns></returns>
        protected double[][] convertMultiArray(double[,] input)
        {
            double[][] retVal = new double[input.GetLength(0)][];
            for (int i = 0; i < input.GetLength(0); i++)
            {
                retVal[i] = new double[input.GetLength(1)];
                for (int j = 0; j < input.GetLength(1); j++)
                {
                    retVal[i][j] = input[i, j];
                }
            }
            return retVal;
        }

        /// <summary>
        /// Converts from a double[][] to a double[,]
        /// </summary>
        /// <param name="input">The array of arrays to convert</param>
        /// <returns></returns>
        protected double[,] convertDoubleArray(double[][] input)
        {
            double[,] retVal = new double[input.Length, input[0].Length];
            for (int i = 0; i < input.Length; i++)
            {
                for (int j = 0; j < input[i].Length; j++)
                {
                    if (input[i].Length != retVal.GetLength(1))
                        throw new ArgumentException("convertDoubleArray must be passed a rectangular matrix!");

                    retVal[i, j] = input[i][j];
                }
            }
            return retVal;
        }

        /// <summary>
        /// Adds TrainingData sets with the same category together
        /// </summary>
        /// <param name="data">List of TrainingData objects to combine</param>
        /// <returns>A list of TrainingData objects with no duplicate category entries</returns>
        protected List<TrainingData> combineCategories(List<TrainingData> data)
        {
            Dictionary<int, TrainingData> combinedData = new Dictionary<int, TrainingData>();
            foreach (TrainingData classifierData in data)
            {
                if (combinedData.ContainsKey(classifierData.Category))
                    combinedData[classifierData.Category].Add(classifierData);
                else
                    combinedData.Add(classifierData.Category, classifierData);
            }

            return combinedData.Values.ToList();
        }

        #endregion

        #region Single Training

        /// <summary>
        /// Trains one SVM using one set of data.  Overwrites any previously saved data for this category.
        /// </summary>
        /// <param name="data">the set of data to use</param>
        /// <returns>the error value returned from the learning algorithm</returns>
        public double TrainSingle(TrainingData data)
        {
            Category category = Category.GetCategory(data.Category);

            // Add data to previousData
            if (previousData.ContainsKey(category))
                previousData[category] = data;
            else
                previousData.Add(category, data);

            double[][] trainingData = data.DataVector;
            
            // Deal with PCA
            if (featureCount != -1)
            {
                double[,] transformedData = convertDoubleArray(trainingData);

                // Create PCA
                KernelPrincipalComponentAnalysis PCA = new KernelPrincipalComponentAnalysis(transformedData, pcaKernel);

                // Add it to the dictionary of PCAs
                if (PCAs.ContainsKey(category))
                    PCAs[category] = PCA;
                else
                    PCAs.Add(category, PCA);

                // "Train" the PCA
                PCA.Compute();

                // Transform the training data
                if (featureCount != 0)
                    trainingData = convertMultiArray(PCA.Transform(transformedData, featureCount));
                else
                    trainingData = convertMultiArray(PCA.Transform(transformedData));
            }

            // Create SVM
            KernelSupportVectorMachine SVM = new KernelSupportVectorMachine(svmKernel, trainingData[0].Length);

            // Add it to the dictionary of SVMs
            if (SVMs.ContainsKey(category))
                SVMs[category] = SVM;
            else
                SVMs.Add(category, SVM);

            // Train the SVM
            /*for (int i = 0; i < trainingData.Length; i++)
            {
                Console.Write("{0} : ", data.Classes[i]);
                foreach (double value in trainingData[i])
                    Console.Write("{0:0.00}, ", value);
                Console.WriteLine();
            }*/
            SequentialMinimalOptimization smo = new SequentialMinimalOptimization(SVM, trainingData, data.Classes);
            //smo.Complexity = complexity;

            return smo.Run();
        }

        /// <summary>
        /// ReTrains one SVM using one set of data.  This method also uses any saved data in this category.
        /// </summary>
        /// <param name="data">the set of data to use</param>
        /// <returns>the error value returned from the learning algorithm</returns>
        public double ReTrainSingle(TrainingData data)
        {
            Category category = Category.GetCategory(data.Category);

            // Add data to previousData
            if (previousData.ContainsKey(category))
            {
                previousData[category].Add(data);
                data = previousData[category];
            }
            else
                previousData.Add(category, data);

            double[][] trainingData = data.DataVector;

            // Deal with PCA
            if (featureCount != -1)
            {
                double[,] transformedData = convertDoubleArray(trainingData);

                // Create PCA
                KernelPrincipalComponentAnalysis PCA = new KernelPrincipalComponentAnalysis(transformedData, pcaKernel);

                // Add it to the dictionary of PCAs
                if (PCAs.ContainsKey(category))
                    PCAs[category] = PCA;
                else
                    PCAs.Add(category, PCA);

                // "Train" the PCA
                PCA.Compute();

                // Transform the training data
                if (featureCount != 0)
                    trainingData = convertMultiArray(PCA.Transform(transformedData, featureCount));
                else
                    trainingData = convertMultiArray(PCA.Transform(transformedData));
            }

            // Create SVM
            KernelSupportVectorMachine SVM = new KernelSupportVectorMachine(svmKernel, trainingData[0].Length);

            // Add it to the dictionary of SVMs
            if (SVMs.ContainsKey(category))
                SVMs[category] = SVM;
            else
                SVMs.Add(category, SVM);

            // Train the SVM
            SequentialMinimalOptimization smo = new SequentialMinimalOptimization(SVM, trainingData, data.Classes);
            //smo.Complexity = complexity;

            return smo.Run();
        }

        #endregion

        #region IGrounder Methods

        /// <summary>
        /// Trains grounder on a list of training data sets.  Overwrites any existing SVMs without
        /// using the saved data.
        /// </summary>
        /// <param name="data">the data with which to train</param>
        public void Train(List<TrainingData> data)
        {
            // Add TrainingData objects with the same category together
            List<TrainingData> combinedData = combineCategories(data);

            // Train each category's classifier
            foreach (TrainingData categoryData in combinedData)
                TrainSingle(categoryData);
        }

        /// <summary>
        /// ReTrains grounder on a list of training data sets.  Builds SVMs while also using previously
        /// saved data.
        /// </summary>
        /// <param name="data">the data with which to train</param>
        public void ReTrain(List<TrainingData> data)
        {
            // Add TrainingData objects with the same category together
            List<TrainingData> combinedData = combineCategories(data);

            // Train each category's classifier
            foreach (TrainingData categoryData in combinedData)
                ReTrainSingle(categoryData);
        }
        
        /// <summary>
        /// Classifies a data vector for each category.
        /// </summary>
        /// <param name="frame">the frame to classify</param>
        /// <returns>a dictionary of each classification with its corresponding confidence value</returns>
        public Dictionary<ILogicElement, double> Test(double[] frame)
        {
            Dictionary<ILogicElement, double> retval = new Dictionary<ILogicElement, double>();

            foreach (Category category in SVMs.Keys)
            {
                double[] testData = frame;
                if(featureCount != -1) {
                    if (featureCount != 0)
                        testData = convertMultiArray(PCAs[category].Transform(convertDoubleArray(new double[][] { testData }), featureCount))[0];
                    else
                        testData = convertMultiArray(PCAs[category].Transform(convertDoubleArray(new double[][] { testData })))[0];
                }
                double categorization = SVMs[category].Compute(testData);
                Console.WriteLine("Category: {0}", categorization);
                int label = Math.Sign(categorization);
                double magnitude = Math.Abs(categorization);

                retval.Add(Label.GetLabel(category, label), magnitude);
            }

            return retval;
        }

        #endregion
    }
}
